local RingKeeper, assert, copy, charId = {}, OneRingLib.xlu.assert, OneRingLib.xlu.copy, OneRingLib.xlu.charId;
local RK_RingDesc, RK_RingData, RK_RingIDs, RK_Version, RK_Rev, SV = {}, {}, {}, 1, 28;
local RK_ManagedRingNames, RK_DeletedRings, RK_FastClick, RK_RotationStore = {};
local RK_ForceRotationUpdate;
local unlocked, queue = false, {}; -- Waiting for SVs

local AB = OneRingLib and OneRingLib.ext and OneRingLib.ext.ActionBook;
assert(AB and AB:compatible(1,1), "A compatible version of ActionBook is required");

-- Ring description -> OneRing propsTable conversion and updates code
local RK_RingPreClick, RK_ParseMacro; do -- Macro parser
	local castAlias = {[SLASH_CAST1]=true,[SLASH_CAST2]=true,[SLASH_CAST3]=true,[SLASH_CAST4]=true,[SLASH_USE1]=true,[SLASH_USE2]=true,[SLASH_CASTSEQUENCE1]=true,[SLASH_CASTSEQUENCE2]=true,[SLASH_CASTRANDOM1]=true,[SLASH_CASTRANDOM2]=true};
	local function RK_MacroSpellIDReplace(sidlist)
		local looseMatch = "";
		for id in sidlist:gmatch("%d+") do
			local sname, srank = GetSpellInfo(tonumber(id));
			local sname2, srank2 = GetSpellInfo(sname or -1);
			if sname and srank == srank2 then
				return sname .. (sname:match("%(") and "()" or "");
			elseif (sname and GetSpellInfo(sname, srank)) or (OneRingLib.xlu.companionSpellCache(sname)) then
				return sname .. "(" .. srank .. ")"
			elseif sname2 then
				looseMatch = sname2 .. "(" .. srank2 .. ")"
			end
		end
		return looseMatch;
	end
	local function RK_RemoveEmptyClauses(clause)
		return clause:gsub("%b[]", ""):match("^%s*;?$") and "" or nil;
	end
	local function RK_MacroLineParse(prefix, command, args)
		if command == "#show" or command == "#showtooltip" or castAlias[command:lower()] then
			args = args:gsub("[^;]+;?", RK_RemoveEmptyClauses);
			return args:match("%S") and prefix .. args or "";
		end
	end
	function RK_ParseMacro(mtext)
		local text = "\n" .. mtext:gsub("{{spell:([%d/]+)}}", RK_MacroSpellIDReplace);
		text = text:gsub("(\n([#/]%S+) ?)([^\n]*)", RK_MacroLineParse):gsub("[%s;]*\n", "\n"):gsub("[%s;]*$", "");
		if text:match("[\n\r]#rkrequire%s*[\n\r]") then return ""; end
		local req = (text:match("[\n\r]#rkrequire %s*([^\n\r]+)") or ""):match("^(%S.-)%s*$");
		if req and not (GetSpellInfo(req) or OneRingLib.xlu.companionSpellCache(req)) then return ""; end
		return (text:gsub("\n#rkrequire[^\n]*", ""):gsub("^\n+", ""));
	end
end
local function RK_IsRelevantRingDescription(desc)
	if not desc then return false; end
	if (desc.class ~= nil and desc.class ~= select(2, UnitClass("player"))) or
		 (desc.limitToChar ~= nil and desc.limitToChar ~= UnitName("player")) then
		return false;
	end
	return true;
end
local function RK_DeferredLoad()
	unlocked = true;
	local pname, deleted, mousemap = UnitName("player"), SV.OPieDeletedRings or {}, {};
	RK_DeletedRings, RK_FastClick, SV.OPieDeletedRings, SV.OPieFastClick = {}, SV.OPieFastClick or {};

	if GetCVarBool("enableWowMouse") then
		mousemap["BUTTON4"], mousemap["BUTTON5"] = "BUTTON12", "BUTTON13";
	end
	RK_RotationStore = {{}, {}, stored={}}
	if type(SV.OPieRotation) == "table" then
		for k,v in pairs(SV.OPieRotation) do
			local c, s, ri = k:match("^([^#]+)#(%d)#([^#]+#[^#]+)$");
			if c == charId then
				RK_RotationStore[tonumber(s)][ri] = v;
			elseif c then
				RK_RotationStore.stored[k] = v;
			end
		end
	end
	SV.OPieRotation = nil;

	for k, v in pairs(queue) do
		if v.hotkey then v.hotkey = v.hotkey:gsub("[^-]+$", mousemap); end
		if deleted[k] == nil and (SV[k] == nil or (SV[k].version or 0) < (v.version or 0)) then
			EC_pcall("RingKeeper", k .. ".AddRingQ", RingKeeper.AddRing, RingKeeper, k, v);
			SV[k] = nil;
		elseif deleted[k] then
			RK_DeletedRings[k] = true;
		end
	end
	for k, v in pairs(SV) do
		EC_pcall("RingKeeper", k .. ".AddRingSV", RingKeeper.AddRing, RingKeeper, k, v);
	end
	collectgarbage("collect");
end
local function RK_SaveRotations(dest)
	for k, v in pairs(RK_RingDesc) do
		if type(v) == "table" and not RK_DeletedRings[k] then
			local i, j = 1, 1;
			while v[i] do local e = v[i];
				if e.type then
					dest[k .. "#" .. i], j = OneRingLib:GetSliceRotation(RK_RingIDs[k], j), j + 1;
				end
				i = i + 1;
			end
		end
	end
end
local function RK_Initializer(event, name, sv)
	if event == "LOGOUT" and unlocked then
		for k, v in pairs(sv) do sv[k] = nil; end

		RK_SaveRotations(RK_RotationStore[GetActiveTalentGroup()]);
		local rotation = RK_RotationStore.stored;
		for i, t in ipairs(RK_RotationStore) do
			for k,v in pairs(t) do
				rotation[charId .. "#" .. i .. "#" .. k] = v;
			end
		end

		for k, v in pairs(RK_RingDesc) do
			if type(v) == "table" and not RK_DeletedRings[k] and v.save then
				for i, v2 in ipairs(v) do
					v2.c = ("%02x%02x%02x"):format((v2.r or 0) * 255, (v2.g or 0) * 255, (v2.b or 0) * 255);
					v2.rtype, v2.type, v2.action, v2.b, v2.g, v2.r, v2.autocaption, v2.autoicon, v2.lastUpdateToken = v2.rtype ~= "spell" and v2.rtype ~= "macrotext" and v2.rtype or nil;
				end
				sv[k] = v;
			end
			if RK_RingIDs[k] then
				local rslice = select(7, OneRingLib:GetRingInfo(RK_RingIDs[k])) or nil;
				RK_FastClick[k] = rslice and RK_RingData[k][rslice] and RK_RingData[k][rslice].rkId or nil;
			end
		end
		sv.OPieDeletedRings, sv.OPieFastClick, sv.OPieRotation = next(RK_DeletedRings) and RK_DeletedRings, next(RK_FastClick) and RK_FastClick, next(rotation) and rotation;
	end
end

local function RK_PullOptions(e, a, ...)
	if a then return e[a], RK_PullOptions(e, ...) end
end
local function RK_SyncRingEntry(e)
	local oldtype, oldaction, oldfc, rtype, rid = e.type, e.action, e.fastClick, e.rtype, e.id;
	e.fastClick, e.type, e.action, e.autocaption, e.autoicon, e.tipType, e.tipDetail = e.fcSlice and true or nil;

	if rtype == "macrotext" then
		local m = RK_ParseMacro(rid)
		if m:match("%S") then e.type = AB:get("macrotext", m) end
	elseif type(rtype) == "string" then
		e.type = AB:get(rtype, rid, RK_PullOptions(e, AB:options(rtype)))
	end

	return e.type ~= oldtype or e.action ~= oldaction or oldfc ~= e.fastClick;
end
local function RK_SyncRing(name, force, tok)
	local desc, ring, changed = RK_RingDesc[name], RK_RingData[name], (force == true);
	if not RK_IsRelevantRingDescription(desc) then return; end
	tok = tok or AB:lastupdate("*")
	if not force and tok == desc.lastUpdateToken then return end
	desc.lastUpdateToken = tok
	if not ring then
		RK_RingData[name] = {};
		ring, changed = RK_RingData[name], true;
	end
	
	local needPreClick;
	for i, e in ipairs(desc) do
		local sliceChanged, sliceNeedsPreClick = RK_SyncRingEntry(e);
		changed, needPreClick = changed or sliceChanged, needPreClick or sliceNeedsPreClick;
	end

	if changed or not RK_RingIDs[name] then
		-- Clear previous ring entries, copy metadata again
		for k, v in pairs(ring) do
			ring[k] = nil;
		end
		for k, v in pairs(desc) do 
			if type(k) ~= "number" then 
				ring[k] = v; 
			end
		end
		
		-- Copy well-defined ring entries from parsed description
		local rot = RK_RotationStore[GetActiveTalentGroup() or 1];
		for i, e in ipairs(desc) do
			if e.type then
				ring[#ring+1] = {e.type, e.action, r=e.r, g=e.g, b=e.b, psbind=e.psbind, caption=e.caption or e.autocaption, icon=e.icon or e.autoicon, rotation=e.rtype == "ring" and rot[name .. "#" .. i] or nil, forceRotation=RK_ForceRotationUpdate, tipType=e.tipType, tipDetail=e.tipDetail, fastClick=e.fastClick, rkId=i};
				if (e.fastClick and RK_FastClick[name] == i) then
					ring.fastClickSlice = #ring;
				end
			end
		end
		ring.preClick = RK_RingPreClick;

		-- Submit ring for update
		if not RK_RingIDs[name] then
			local ok, id = EC_pcall("OPie.CRSync", name, OneRingLib.CreateRing, OneRingLib, name, ring);
			if ok then
				RK_RingIDs[name], RK_RingIDs[id] = id, name;
			end
		else
			EC_pcall("OPie.CRSync", name, OneRingLib.SetRingData, OneRingLib, RK_RingIDs[name], ring);
		end
	end
end
local function RK_SanitizeDescription(props)
	for i, v in ipairs(props) do
		if v.c and not (v.r and v.g and v.b) then
			local r,g,b = v.c:match("^(%x%x)(%x%x)(%x%x)$");
			if r then
				v.r, v.g, v.b = tonumber(r, 16)/255, tonumber(g, 16)/255, tonumber(b, 16)/255;
			end
		end
		if v.rtype == "nitem" then
			v.rtype, v.byName = "item", true
		elseif v.rtype == nil and type(v.id) == "number" then
			v.rtype = "spell"
		elseif v.rtype == nil and type(v.id) == "string" then
			v.rtype = "macrotext"
		elseif v.rtype == "companion" then
			v.rtype = "spell"
		end
		v.type, v.action, v.onlyWhilePresent = nil
	end
	return props;
end
function RK_RingPreClick(ring)
	RK_SyncRing(RK_RingIDs[ring]);
end
local function RK_SoftUpdateAll()
	for k,v in pairs(RK_RingDesc) do
		EC_pcall("RK.Sync", "Ring " .. k, RK_SyncRing, k);
	end
end
local function RK_SwitchSpec(event, newSpec, oldSpec)
	if not unlocked then return; end
	RK_SaveRotations(RK_RotationStore[oldSpec]);
	RK_ForceRotationUpdate = true;
	RK_SoftUpdateAll()
	RK_ForceRotationUpdate = nil;
end

-- Public API
function RingKeeper:AddRing(name, desc)
	assert(type(name) == "string" and type(desc) == "table", "Syntax: RingKeeper:AddRing(name, descTable);", 2);
	assert(RK_RingDesc[name] == nil, "Ring %q is already described.", 2, name);
	assert(unlocked == true or not queue[name], "Ring %q is already described.", 2, name);
	if not unlocked then
		queue[name] = desc;
		return;
	end
	local rid = RK_RingIDs[name] or (#RK_ManagedRingNames + 1);
	RK_ManagedRingNames[rid], RK_RingDesc[name], RK_DeletedRings[name] = name, RK_SanitizeDescription(copy(desc)), nil;
	RK_SyncRing(name, true);
end
function RingKeeper:ModifyRing(name, desc)
	assert(type(name) == "string" and type(desc) == "table", "Syntax: RingKeeper:ModifyRing(name, descTable);", 2);
	assert(RK_RingDesc[name], "Ring %q is not described.", 2, name);
	desc.save = true; -- modifications must be saved.
	RK_RingDesc[name] = RK_SanitizeDescription(copy(desc, nil, RK_RingDesc[name]));
	RK_SyncRing(name, true);
end
function RingKeeper:RemoveRing(name)
	assert(type(name) == "string", "Syntax: RingKeeper:RemoveRing(name);", 2);
	assert(RK_RingDesc[name], "Ring %q is not described.", 2, name);
	if RK_RingIDs[name] then
		-- Replace ring with dummy, unbind.
		OneRingLib:SetRingData(RK_RingIDs[name], {name="Remnant", removed=true, internal=true});
		OneRingLib:SetRingBinding(RK_RingIDs[name], nil);
		OneRingLib:ClearRingOptions(RK_RingIDs[name]);
	end
	RK_RingDesc[name], SV[name], RK_DeletedRings[name] = nil, nil, true;

	for i,n in ipairs(RK_ManagedRingNames) do
		if n == name then
			table.remove(RK_ManagedRingNames, i);
			return;
		end
	end
end
function RingKeeper:GetRingDescription(name)
	assert(type(name) == "string", "Syntax: descTable = RingKeeper:GetRingDescription(name);", 2);
	local ring = assert(RK_RingDesc[name], "Ring %q is not described.", 2, name);
	return copy(ring);
end
function RingKeeper:GetVersion()
	return RK_Version, RK_Rev;
end
function RingKeeper:GetManagedRings()
	return #RK_ManagedRingNames;
end
function RingKeeper:GetManagedRingName(id)
	assert(type(id) == "number", "Syntax: name, hname, active, numSlices = RingKeeper:GetManagedRingName(index);", 2);
	local id = assert(RK_ManagedRingNames[id], "Index out of range", 2);
	return id, RK_RingDesc[id].name or id, RK_RingIDs[id] ~= nil, #RK_RingDesc[id];
end
function RingKeeper:RestoreDefaults()
	for k, v in pairs(queue) do
		if RK_IsRelevantRingDescription(v) then -- Do not reset rings that cannot be "seen".
			if RK_RingDesc[k] ~= nil then
				self:ModifyRing(k, queue[k]);
			else
				self:AddRing(k, queue[k]);
			end
		end
	end
end
function RingKeeper:GenFreeRingName(base)
	assert(type(base) == "string", "Syntax: name = RingKeeper:GenFreeRingName(\"base\");", 2);
	base = base:gsub("[^%a%d]", "");
	if base:match("^OPie") or not base:match("^%a") then base = "x" .. base; end
	local ap, c = "", 1;
	while RK_RingDesc[base .. ap] or SV[base .. ap] do ap, c = math.random(2^c), c+1; end
	return base .. ap;
end

if type(OneRingLib) == "table" then
	OneRingLib.ext.RingKeeper = RingKeeper;
	SV = OneRingLib:RegisterPVar("RingKeeper", SV, RK_Initializer);
	EC_Register("PLAYER_LOGIN", "RingKeeper.DeferredLoad", RK_DeferredLoad);
	EC_Register("PLAYER_REGEN_DISABLED", "RingKeeper.Items", RK_SoftUpdateAll);
	EC_Register("ACTIVE_TALENT_GROUP_CHANGED", "RingKeeper.SwitchSpec", RK_SwitchSpec);
end